3D Model Loading
Introduction
In this article you will learn how to implement a 3DS file loader, and if all goes well you will be able to implement an MD2 file loader after this on your own. When you want to implement a file loader you need the technical specifications of the given file format and the best place to start searching is the Wotsit.org website. But what happens if you can't find the specifications? Well then you have to reverse engineer the file format, which is not an easy task at all, mostly because data is sleeping creature, and code is a living creature. From this is is obvious that reversing living code is much easier because you can see its behaviour and everything, while data is living only when software makes use of it. Anyway in our case the 3DS file format, we have the specs, so no need to go into reversing. If you search on the internet for a 3DS loader you will find quite different approaches on doing this, and a lot of them uses recursion. In my humble opinion recursion just slows down things and nothing can replace a nice LOOP and a SWITCH, and also this will allow us to keep the whole loader into one function.
A few notes before we begin
The loader we will go to create will be quite simple, it will just read all the meshes from the file and it will render it in eighter wireframe solid or textured mode. Implementing a texture loader from scratch is beyond the scope of this article so I won't go into that, and it's up to you to add the texture binding. However, the texture coordinates will be loaded and applied into the rendered meshes. We won't read material information from the file, because in a Demo or Game you will use a Quake3 Style Software Shader system or hardware accelerated vertex or pixel shaders, so it doesn't makes sense. Normals will be also missing and you will have to calculate them yourself. You can read about this at the end of the article, in the Evil Genious Tip - How to calculate normals? box. Now we can move into the code itself, and it may appear a bit strange, but I will explain things by using inline comments. Let's rock'n'roll!
The actual implementation
Model3DS.H - Everything is explained inside the actual code as comments.
/*
Public Domain Code
Coded by The IceBreaker
*/
#ifndef __model3ds_h__
#define __model3ds_h__
#include <windows.h>
#include <gl\gl.h>
#include <gl\glu.h>
#include <stdio.h>
/* it is more convenient to define our own boolean :D */
typedef enum {false,true} nboolean;
/* magic numbers from the 3DS specification */
#define MAIN_CHUNK 0x4d4d
#define VERSION 0x0002
#define EDITOR_CHUNK 0x3d3d
#define OBJECT_BLOCK 0x4000
#define TRIANGULAR_MESH 0x4100
#define VERTICES_LIST 0x4110
#define FACES_DESCRIPTION 0x4120
#define FACES_MATERIAL_LIST 0x4130
#define MAPPING_COORDINATES_LIST 0x4140
/*
As you probably know the smallest element
of the 3D space is the vertex.
A vertex usually is defined by 3 coordinates,
but this time we include the texture coordinates
too, commonly named U and V.
*/
typedef struct t_Vertex
{
float x,y,z;
float u,v;
} Vertex;
/*
A face (polygon, triangle) in our case contains indices
to 3 vertices. We are storing indices because this way
multiple faces can share the same vertices. A 3DS file
always stores faces AS triangles (and so do most of the
3D file formats) so the number of indices is always three.
If you will want to implement your own file format then
it is useful to make the number of indices variable.
*/
typedef struct t_Face
{
unsigned short a,b,c;
} Face;
/*
A mesh is a collection of Vertices and Faces, but it also
holds a name. These are stored in simple dynamic arrays.
Storing the number of vertices respectivly faces, it is
not that important because this number could be calculated
from the allocated array, but it is more convenient this way.
To calculate the length of a dynamic array you do this:
int length = (sizeof(myarray[0])/sizeof(myarray));
In *human* language, you divide the size of the first element
with the size of the whole array.
*/
typedef struct t_Mesh
{
char MeshName[64];
unsigned short NumVertices;
unsigned short NumFaces;
Vertex *Vertices;
Face *Faces;
} Mesh;
/*
As I said before a 3DS file can contain multiple meshes, so the
Model structure holds a dynamic array of meshes, a model name,
and two vectors to positionate and rotate the model.
The last two are not required for the loading part, but for
rendering.
*/
typedef struct t_Model
{
char ModelName[64];
/* Added for rendering */
float position[3];
float rotation[3];
unsigned short NumMeshes;
Mesh *Meshes;
} Model;
/*
To see the implementation of these functions, proceed to
the model3ds.c implementation file.
*/
nboolean model_Load (Model *model, const char *FileName);
void model_Free (Model *model);
void model_Render (Model *model);
void model_Rotate (Model *model, float x, float y, float z);
#endif /* !__model3ds_h__ */
Model3DS.C - Everything is explained inside the actual code as comments.
/*
Public Domain Code
Coded by The IceBreaker
*/
#include "model3ds.h"
/*
This function will fill the given model structure
with the contents of the file specified by FileName.
*/
nboolean model_Load(Model *model, const char *FileName)
{
int i;
FILE *pFile;
unsigned short usChunkID;
unsigned int uiChunkLength;
unsigned char uChar;
unsigned short usNum;
unsigned short usFaceFlags;
unsigned int uiVersion;
unsigned short uiBytesRead;
unsigned short a,b,c;
float x,y,z,u,v;
/* Set the number of meshes inside the model to 0 */
model->NumMeshes = 0;
if ((pFile=fopen(FileName, "rb"))==NULL) return false;
/* Get the file size */
long int iOldPos = fseek(pFile,0L,SEEK_END);
long int iFileLength = ftell(pFile);
fseek(pFile,iOldPos,SEEK_SET);
/* Loop until current position is smaller than the file size */
while (ftell(pFile)<iFileLength)
{
/*
Read the ChunkID and ChunkLength.
We are catching the number of bytes readed
by these are not important in our case, we
could also write the first line as:
fread(&usChunkID,sizeof(unsigned short),1,pFile);
*/
uiBytesRead=fread(&usChunkID,1,sizeof(unsigned short),pFile);
uiBytesRead+=fread(&uiChunkLength,1,sizeof(unsigned int),pFile);
switch (usChunkID)
{
/*
We end up here only once, but we don't need to
process anything anyway.
*/
case MAIN_CHUNK:
break;
/*
It is good to check for the version number.
If it is bigger than 3 we just display a
warning messagebox, but we continue as usual.
*/
case VERSION:
{
/* version checking */
uiBytesRead+=fread(&uiVersion,1,sizeof(unsigned int),pFile);
if (uiVersion > 3)
{
MessageBox(0,"File version is >3, the file may not load correctly!","Warning",MB_ICONWARNING);
}
}
break;
/*
We end up here only once, but we don't need to
process anything anyway.
*/
case EDITOR_CHUNK:
break;
/*
The OBJECT_BLOCK chunk is a very important one,
because here we alloc and resize the dynamic
array of meshes, and also we read the actual
mesh name from the file.
*/
case OBJECT_BLOCK:
{
if (model->NumMeshes == 0)
{
model->NumMeshes++;
model->Meshes = (Mesh *) malloc(model->NumMeshes*sizeof(Mesh));
}
else
{
model->NumMeshes++;
model->Meshes = (Mesh *) realloc(model->Meshes,model->NumMeshes*sizeof(Mesh));
}
i=0;
do
{
uiBytesRead+=fread(&uChar,1,sizeof(unsigned char),pFile);
model->Meshes[model->NumMeshes-1].MeshName[i]=uChar;
i++;
}while(uChar != '\0' && i<64);
}
break;
/*
We endup here multiple times, but we don't give
a damn' ... hehe
*/
case TRIANGULAR_MESH:
break;
/*
This chunk contains the actual "meat" of the file, the VERTICES.
*/
case VERTICES_LIST:
{
/* We already allocated meshes?!!! */
if (model->NumMeshes > 0)
{
/* Get get the number of vertices for the current mesh */
uiBytesRead+=fread(&usNum, 1, sizeof(unsigned short), pFile);
/* Allocate the dynamic vertex array */
model->Meshes[model->NumMeshes-1].Vertices = (Vertex *) malloc(usNum*sizeof(Vertex));
memset(model->Meshes[model->NumMeshes-1].Vertices,0,usNum*sizeof(Vertex));
/*
Read the vertices as three floating point values,
and store them inside the allocated array.
*/
for (i=0; i<usNum; i++)
{
uiBytesRead+=fread(&x,1, sizeof(float),pFile);
uiBytesRead+=fread(&y,1, sizeof(float),pFile);
uiBytesRead+=fread(&z,1, sizeof(float),pFile);
model->Meshes[model->NumMeshes-1].Vertices[i].x = x;
model->Meshes[model->NumMeshes-1].Vertices[i].y = y;
model->Meshes[model->NumMeshes-1].Vertices[i].z = z;
}
/*
Finally store the number of allocated vertices.
*/
model->Meshes[model->NumMeshes-1].NumVertices = usNum;
}
}
break;
/*
This chunk contains the face indices.
*/
case FACES_DESCRIPTION:
{
/* We already allocated meshes?!!! */
if (model->NumMeshes > 0)
{
/* Get get the number of faces (triangles) for the current mesh */
uiBytesRead+=fread(&usNum,1,sizeof(unsigned short),pFile);
/* Allocate the dynamic vertex array */
model->Meshes[model->NumMeshes-1].Faces = (Face *) malloc(usNum*sizeof(Face));
memset(model->Meshes[model->NumMeshes-1].Faces,0,usNum*sizeof(Face));
/*
Read the face indices as 3 unsigned shorts + 1 unsigned short used as
a flag, but which is not important to us, but we have to read it,
because we like the KISS principle.
Then obviously we store only the three indices inside the
allocated array.
*/
for (i=0;i<usNum; i++)
{
uiBytesRead+=fread(&a,1,sizeof(unsigned short),pFile);
uiBytesRead+=fread(&b,1,sizeof(unsigned short),pFile);
uiBytesRead+=fread(&c,1,sizeof(unsigned short),pFile);
uiBytesRead+=fread(&usFaceFlags,1,sizeof(unsigned short),pFile);
model->Meshes[model->NumMeshes-1].Faces[i].a = a;
model->Meshes[model->NumMeshes-1].Faces[i].b = b;
model->Meshes[model->NumMeshes-1].Faces[i].c = c;
}
/*
Finally store the number of allocated faces.
*/
model->Meshes[model->NumMeshes-1].NumFaces = usNum;
}
}
break;
/*
This chunk contains the actual texture coordinates for
the current mesh.
The number of texture coordinates is equal to the number
of vertices inside the mesh.
*/
case MAPPING_COORDINATES_LIST:
{
/* We already allocated meshes?!!! */
if (model->NumMeshes > 0)
{
/* Read the number of texture coordinates ( == NumVertices ) */
uiBytesRead+=fread(&usNum,1,sizeof(unsigned short), pFile);
/*
Read the coordinates as two floating point values
and store them inside the previously allocated
dynamic vertex array.
*/
for (i=0; i<usNum; i++)
{
uiBytesRead+=fread(&u,1,sizeof(float),pFile);
uiBytesRead+=fread(&v,1,sizeof(float),pFile);
model->Meshes[model->NumMeshes-1].Vertices[i].u = u;
model->Meshes[model->NumMeshes-1].Vertices[i].v = v;
}
}
}
break;
default:
/*
We deal like this with unknow chunks ... Grrr
If we end up here uiBytesRead == 6, so we could
write uiChunkLengh-6 instead.
How do I know that it will be 6?
It is simple.At the beginning of the loop we read
6 bytes in total. One unsigned short which is 2 bytes
and one unsigned int which is 4 bytes.
*/
fseek(pFile, uiChunkLength-uiBytesRead, SEEK_CUR);
}
}
fclose(pFile);
/*
Play a bit with the position
and rotation of the whole
model.
*/
model->position[0] = 0.0f;
model->position[1] = 0.0f;
model->position[2] = -200.0f;
model->rotation[0] = -90.0f;
model->rotation[1] = 0.0f;
model->rotation[2] = 0.0f;
/*
Assign the name of the first mesh
as the global mesh name.
*/
if (model->NumMeshes > 0)
strcpy(model->ModelName, model->Meshes[0].MeshName);
return true; /* The model was loaded! */
}
/*
This function will free the dynamic
array allocated inside the given
model structure.
*/
void model_Free(Model *model)
{
int i;
/* No mesh found in the model */
if(!model->Meshes)
return;
for(i=0;i<model->NumMeshes;i++)
{
free(model->Meshes[i].Faces);
free(model->Meshes[i].Vertices);
}
free(model->Meshes);
}
/*
This function will render the model inside
the given model structure.
*/
void model_Render(Model *model)
{
int i,index,index2,k;
glPushMatrix();
/*
Positionate and rotate.
*/
glTranslatef(model->position[0],model->position[1],model->position[2]);
glRotatef(model->rotation[0],1.0f,0.0f,0.0f);
glRotatef(model->rotation[1],0.0f,1.0f,0.0f);
glRotatef(model->rotation[2],0.0f,0.0f,1.0f);
glBegin(GL_TRIANGLES);
/* Loop through the meshes */
for (i=0;i<model->NumMeshes;i++)
{
/*
Loop through the faces of the current mesh.
You could also bind textures the each mesh here.
*/
for (index=0;index<model->Meshes[i].NumFaces;index++)
{
/*
If you would want just face normals, then
you should add only 1 glNormal3f()
after the next line.
For vertex normals you should add one
glNormal3f() before each glTexCoord2f().
*/
index2 = model->Meshes[i].Faces[index].a;
glTexCoord2f(
model->Meshes[i].Vertices[index2].u,
model->Meshes[i].Vertices[index2].v
);
glVertex3f (
model->Meshes[i].Vertices[index2].x,
model->Meshes[i].Vertices[index2].y,
model->Meshes[i].Vertices[index2].z
);
index2 = model->Meshes[i].Faces[index].b;
glTexCoord2f(
model->Meshes[i].Vertices[index2].u,
model->Meshes[i].Vertices[index2].v
);
glVertex3f (
model->Meshes[i].Vertices[index2].x,
model->Meshes[i].Vertices[index2].y,
model->Meshes[i].Vertices[index2].z
);
index2 = model->Meshes[i].Faces[index].c;
glTexCoord2f(
model->Meshes[i].Vertices[index2].u,
model->Meshes[i].Vertices[index2].v
);
glVertex3f (
model->Meshes[i].Vertices[index2].x,
model->Meshes[i].Vertices[index2].y,
model->Meshes[i].Vertices[index2].z
);
}
}
glEnd();
glPopMatrix();
}
/*
This function will increment the rotation vector's
elements with the given x,y or z.
*/
void model_Rotate(Model *model,float x, float y, float z)
{
model->rotation[0] += x;
model->rotation[1] += y;
model->rotation[2] += z;
}
Duhhh ... that was long. I don't want to make this article even longer, so I won't list the small example program which will make use of the library, to load and render a mesh selected by the user.
A few notes after we finished
The loader presented in this article will load and display any file exported by Anim8or, and possibly other 3D Editors, but I am not sure if it will load just any 3DS file. Anyway I hope that you got the concept and you will be able to use an improved loader in your own game/demo based on this one. This can be integrated seemlessly into any project. Another thing which I want to highlight is that there is no Endian conversion, mainly because again I wanted to stick to the KISS principle and I know that most of the dudes out there use the x86 architecture. I used GCC to compile, thanks for reading and Good Night.
Calculating normals is fairly easy when the data is in our structures defined above. First to calculate
face normals we loop through the faces and we get the position (vector) of the two vertices in the face and we substract from these the
third one. Then we do the cross-product of these two vectors. Finally we normalize it and we have the face
normal.
To calculate the vertex normals we loop through the vertices, and we sum up the normals from the faces which share the current vertex, then we divide the resulted vector with the number of shared faces. Finally we normalize it, and there is the vertex normal.